59. 协方差分析

本章概要

  • 学习材料:2019—2025中国资产日价格、周收益、权重与课堂风险预算。
  • 本章任务:计算四只股票的协方差矩阵和组合方差,观察 60 个交易日滚动相关,并比较不同时间窗口和判断标准下的结果。
  • 完成后你将得到:四只股票全部六组配对的相关系数、组合方差,以及不同时间窗口下的比较表。
  • 自我检查:用双重求和再次计算组合方差,并确认股票、日期、权重和价格数据均符合题目要求。
  • 拓展练习:把同样的方法应用到五粮液、恒瑞医药、长江电力和美的集团,比较六组股票配对的相关关系和组合风险。

本章概览:为什么要研究变量间的关系?

协方差(Covariance)和相关系数(Correlation)是衡量两个变量关系的基础统计量。

在金融领域,这些指标帮助我们:

  • 量化风险:投资组合的风险取决于资产间的协方差
  • 分散化:选择低相关资产降低组合风险
  • 因子识别:发现驱动资产收益的共同因子
  • 风险对冲:寻找负相关资产进行对冲

先手算“共同偏离”,再进入真实矩阵

设三期收益(百分点)为 \(X=(1,2,3)\)\(Y=(2,4,6)\)。两者均值为 2 与 4,偏离量分别为 \((-1,0,1)\)\((-2,0,2)\)

\[ \large s_{XY}=\frac{(-1)(-2)+0\times0+1\times2}{3-1}=2 \]

同号偏离乘积为正,所以样本协方差为正;若把 \(Y\) 改成 \((6,4,2)\),乘积改为负,协方差也为负。

协方差公式:每个符号都对应手算表

\[ \large s_{XY}=\frac{1}{n-1}\sum_{i=1}^{n}(x_i-\bar{x})(y_i-\bar{y}) \]

  • \(n\):配对观测数;\(i\):同一期配对索引;\(x_i,y_i\):第 \(i\) 对观测。
  • \(\bar{x},\bar{y}\):各变量样本均值;分母 \(n-1\) 对应样本协方差。
  • 协方差保留单位,适合进入 \(w^\top\Sigma w\);相关系数除以 \(s_Xs_Y\) 后无量纲,适合跨资产对比较。

零相关不是“没有关系”

\(X=(-1,0,1)\)\(Y=X^2=(1,0,1)\),共同偏离乘积之和为零,所以样本协方差与 Pearson 相关为零;但 \(Y\) 完全由 \(X\) 决定。

选择边界:先画散点图并检查离群点;线性风险聚合报告协方差,跨量纲关系强度报告相关。零相关只表示没有检测到线性关系。

进阶真实案例:长三角组合风险矩阵

要素 本章设置
本地资产 stock_price_pre_adjusted.h5stock_basic_data.h5
样本/字段 2019–2025 海康威视等四公司日收益率(close,共同交易日对齐)
判断方法 每周五记录 60 个交易日滚动相关;如果连续四周高于 0.8,再检查组合是否过度集中

若两资产 60 日相关系数连续四周高于 0.8,则不能视为独立分散来源;季度再次检查。随机组合仅解释算法,不作市场依据。

本地行情分析:真实收益率相关矩阵

Listing 1: 长三角代表公司真实复权收益率相关矩阵
展开周度满足条件与组合风险代码
from pathlib import Path  # 导入Path以兼容两种课程运行平台
import numpy as np  # 导入NumPy用于矩阵与有限值计算
import pandas as pd  # 导入Pandas用于行情整理与关系计算
linux_root = Path('/home/ubuntu/r2_data_mount/data')  # 指定Linux课程数据根目录
windows_root = Path(r'C:\qiufei\data')  # 指定Windows课程数据根目录
data_root = linux_root if linux_root.exists() else windows_root  # 选择当前可用根目录
price_path = data_root / 'stock' / 'stock_price_pre_adjusted.h5'  # 定位前复权行情快照
basic_path = data_root / 'stock' / 'stock_basic_data.h5'  # 定位公司基础信息文件
target_codes = ['002415.SZ','600276.SH','600104.SH','002230.SZ']  # 定义四只长三角非金融公司样本
expected_names = {'002415.SZ':'海康威视','600276.SH':'恒瑞医药','600104.SH':'上汽集团','002230.SZ':'科大讯飞'}  # 定义待核验代码名称映射
is_price_available = price_path.exists()  # 记录行情数据要求状态
if not is_price_available:  # 行情文件缺失时提示读者下载
    print(f'未找到课程数据文件:{price_path};请从课程数据下载入口获取后重新运行。')  # 提示读者核对文件位置
    print('数据读取成功后再计算滚动相关和组合方差。')  # 说明下一步

本地行情分析 1/7:公司名称核验

Listing 2
展开公司名称核验
name_audit = {'status':'NOT_VERIFIED','observed_names':{}}  # 初始化名称未核验状态
if is_price_available and basic_path.exists():  # 公司基础信息存在时检查名称
    companies = pd.read_hdf(basic_path,key='stock_basic_info').rename(columns={'order_book_id':'ts_code','symbol':'name'})  # 读取并整理公司基础信息
    companies['ts_code'] = companies['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一证券代码后缀
    name_column = next((column for column in ['name','股票简称','sec_name'] if column in companies.columns),None)  # 选择可用名称字段
    if name_column is None or 'ts_code' not in companies.columns:  # 核验名称与代码字段要求
        raise ValueError('公司基本信息中缺少 ts_code 或公司名称字段')  # 提示缺少的字段
    observed_names = companies.loc[companies['ts_code'].isin(target_codes),['ts_code',name_column]].drop_duplicates('ts_code').set_index('ts_code')[name_column].to_dict()  # 提取目标代码名称
    mismatched_names = {code:observed_names.get(code) for code,name in expected_names.items() if observed_names.get(code)!=name}  # 识别不一致映射
    if mismatched_names:  # 名称不一致时停止使用真实公司标签
        raise ValueError(f'股票代码与公司名称不匹配:{mismatched_names}')  # 提示不一致的记录
    name_audit = {'status':'PASS','observed_names':observed_names}  # 保存通过的名称核验结果
elif is_price_available:  # 仅行情可用时保留未核验状态
    print(f'未找到公司基本信息文件:{basic_path};请下载后重新核对公司名称。')  # 提示读者下载文件

本地行情分析 2/7:同一时间段的价格面板

Listing 3
展开同一时间段的价格面板构造
if is_price_available:  # 行情存在时读取同一时间段真实价格
    rq_codes = [code.replace('.SH','.XSHG').replace('.SZ','.XSHE') for code in target_codes]  # 转换为HDF证券代码
    prices = pd.read_hdf(price_path,key='data',where='order_book_id in rq_codes',columns=['close']).reset_index().rename(columns={'order_book_id':'ts_code','date':'trade_date'})  # 读取目标前复权收盘价
    prices['ts_code'] = prices['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 恢复课堂证券代码后缀
    prices['trade_date'] = pd.to_datetime(prices['trade_date'])  # 统一交易日期类型
    sample = prices.loc[prices['ts_code'].isin(target_codes)&prices['trade_date'].between('2019-01-01','2025-12-31')]  # 固定样本资产与期间
    observed_codes = set(sample['ts_code'].dropna().unique())  # 收集实际到齐资产代码
    missing_codes = sorted(set(target_codes)-observed_codes)  # 识别缺失目标资产
    if missing_codes:  # 资产未全部到齐时停止矩阵计算
        raise ValueError(f'价格数据中缺少这些股票:{missing_codes}')  # 提示缺少的股票
    if sample.duplicated(['trade_date','ts_code']).any():  # 检查资产日期键唯一性
        raise ValueError('同一股票与交易日存在重复记录')  # 提示重复记录
    raw_close_panel = sample.pivot(index='trade_date',columns='ts_code',values='close').reindex(columns=target_codes)  # 构造固定顺序价格面板
    missing_observations = raw_close_panel.isna().sum()  # 统计各资产缺失观测
    close_panel = raw_close_panel.dropna()  # 仅保留共同交易日
    if close_panel.empty:  # 无共同交易日时停止分析
        raise ValueError('这些股票之间没有共同交易日,请核对日期范围')  # 提示核对日期

本地行情分析 3/7:收益率关系矩阵

Listing 4
展开收益率关系矩阵
if is_price_available:  # 数据就绪时计算收益与关系矩阵
    return_panel = close_panel.pct_change().dropna()  # 计算共同交易日日收益率
    covariance_matrix = return_panel.cov()  # 计算全样本协方差矩阵
    correlation_matrix = return_panel.corr()  # 计算全样本相关矩阵
    if len(return_panel)<80:  # 保证60日窗口与四周满足条件所需长度
        raise ValueError('共同交易日不足,无法评估60日窗口与连续四周满足条件')  # 样本不足时停止

本地行情分析 4/7:单资产对周度函数

Listing 5
展开单资产对周度状态函数
def summarize_weekly_pair(returns_frame,left_code,right_code):  # 定义单资产对周度状态函数
    rolling_pair = returns_frame[left_code].rolling(60).corr(returns_frame[right_code]).dropna()  # 计算60日滚动相关
    weekly_pair = rolling_pair.resample('W-FRI').last().dropna()  # 转为周末最后状态
    if len(weekly_pair)<4:  # 核验连续四周所需样本
        raise ValueError(f'{left_code}/{right_code}不足四周数据')  # 提示样本不足
    weekly_breach = weekly_pair.gt(0.8)  # 标记正相关超课堂阈值状态
    breach_streak = weekly_breach.groupby((~weekly_breach).cumsum()).cumsum().astype(int)  # 计算连续满足条件周数
    pair_audit = pd.DataFrame({'asset_pair':f'{left_code} / {right_code}','week_end':weekly_pair.index,'correlation_60d':weekly_pair.values,'positive_concentration':weekly_breach.values,'continuous_weeks':breach_streak.values})  # 生成完整周度检查表
    current_state = {'asset_pair':f'{left_code} / {right_code}','week_end':weekly_pair.index[-1],'correlation_60d':weekly_pair.iloc[-1],'positive_threshold':.8,'continuous_weeks':int(breach_streak.iloc[-1]),'four_week_condition_met':bool(breach_streak.iloc[-1]>=4)}  # 生成当前资产对高正相关状态
    return pair_audit.tail(4).to_dict('records'),current_state  # 返回最近四周检查与当前状态

本地行情分析 5/7:全部资产对状态

Listing 6
展开全部资产对周度状态
if is_price_available:  # 数据就绪时汇总全部资产对
    weekly_rows = []  # 初始化当前状态容器
    weekly_audit_rows = []  # 初始化最近四周检查容器
    for left_position,left_code in enumerate(return_panel.columns):  # 逐个选择左侧资产
        for right_code in return_panel.columns[left_position+1:]:  # 仅遍历不重复右侧资产
            pair_audit_rows,pair_state = summarize_weekly_pair(return_panel,left_code,right_code)  # 计算单资产对状态
            weekly_audit_rows.extend(pair_audit_rows)  # 累计资产对最近四周检查
            weekly_rows.append(pair_state)  # 累计资产对当前状态
    weekly_state_table = pd.DataFrame(weekly_rows)  # 汇总当前周度状态表
    weekly_audit_table = pd.DataFrame(weekly_audit_rows)  # 汇总最近四周检查表
    if len(weekly_state_table)!=6 or weekly_state_table['asset_pair'].nunique()!=6:  # 核验四资产全部六个唯一资产对
        raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 资产对覆盖不足时停止

本地行情分析 6/7:判断组合风险

Listing 7
展开组合风险与满足条件计算
if is_price_available:  # 数据就绪时计算课堂组合风险
    portfolio_weights = pd.Series([.25,.25,.25,.25],index=target_codes,name='weight').reindex(return_panel.columns)  # 建立等权课堂组合并对齐资产顺序
    if len(portfolio_weights)!=return_panel.shape[1] or portfolio_weights.isna().any():  # 核验权重长度与资产顺序
        raise ValueError('权重数量或股票顺序与收益率矩阵不一致')  # 提示核对权重
    if (portfolio_weights<0).any() or not np.isclose(portfolio_weights.sum(),1.0):  # 核验非负与和为1
        raise ValueError('权重必须为非负数且合计为 1')  # 提示权重要求
    annual_covariance = return_panel.tail(60).cov()*252  # 用课堂252因子年化最近60日协方差
    portfolio_variance = float(portfolio_weights@annual_covariance@portfolio_weights)  # 计算组合年化方差
    risk_budget = .04  # 设置年化20%波动对应的课堂方差预算假设
    correlation_condition_met = bool(weekly_state_table['four_week_condition_met'].any())  # 判断是否存在连续四周高正相关
    variance_condition_met = bool(portfolio_variance>risk_budget)  # 判断组合方差是否超过课堂预算
    portfolio_condition_met = correlation_condition_met or variance_condition_met  # 合并两类核对判断条件

本地行情分析 7/7:比较不同设置

Listing 8
展开处理建议与敏感性核对
if is_price_available:  # 数据就绪时整理结果并比较不同设置
    high_correlation_pairs = weekly_state_table.loc[weekly_state_table['four_week_condition_met'],'asset_pair'].tolist()  # 提取当前连续四周高正相关资产对
    decision_table = pd.DataFrame([{'state_window_days':60,'state_sampling':'W-FRI','sensitivity_windows_weeks':'20/60/120','annualization_factor':252,'variance_unit':'年化日收益方差','portfolio_variance':portfolio_variance,'risk_budget':risk_budget,'positive_threshold':.8,'correlation_condition':'correlation_60d > 0.8 连续四周','high_correlation_pairs':high_correlation_pairs,'correlation_condition_met':correlation_condition_met,'variance_condition_met':variance_condition_met,'condition_met':portfolio_condition_met,'owner':'风控负责人','action':'核对共同因子并降低集中权重' if portfolio_condition_met else '保持权重并按周观察','review_note':'全部资产对不再连续四周高于0.8且年化方差回到0.04内;下一季度再次检查'}])  # 整理课堂结果和建议
    weekly_return_panel = close_panel.resample('W-FRI').last().pct_change(fill_method=None).dropna()  # 从真实周末价格生成周收益,避免把交易日误称为周
    sensitivity_rows = []  # 初始化周窗口与阈值敏感性结果
    for window_weeks in [20,60,120]:  # 比较三种事先设定周收益窗口
        for positive_threshold in [.7,.8,.9]:  # 比较三档课堂正相关阈值
            qualifying_pair_count = 0  # 统计曾满足连续四周条件的资产对数
            qualifying_week_count = 0  # 统计全部资产对的满足条件周数
            for left_position,left_code in enumerate(weekly_return_panel.columns):  # 逐个选择左侧资产
                for right_code in weekly_return_panel.columns[left_position+1:]:  # 仅遍历不重复右侧资产
                    rolling_weekly_correlation = weekly_return_panel[left_code].rolling(window_weeks).corr(weekly_return_panel[right_code])  # 按周收益窗口计算相关序列
                    weekly_threshold_breach = rolling_weekly_correlation.gt(positive_threshold)  # 标记正相关越过课堂阈值的周
                    four_week_condition_met = weekly_threshold_breach.rolling(4,min_periods=4).sum().eq(4)  # 仅连续四周越界时满足条件
                    qualifying_pair_count += int(four_week_condition_met.any())  # 汇总曾满足条件的资产对
                    qualifying_week_count += int(four_week_condition_met.sum())  # 汇总持续满足条件的周数
            sensitivity_rows.append({'window_weeks':window_weeks,'positive_threshold':positive_threshold,'pair_count':6,'qualifying_pairs':qualifying_pair_count,'qualifying_weeks':qualifying_week_count})  # 保存覆盖全部6对的频率清晰敏感性情景
    sensitivity_table = pd.DataFrame(sensitivity_rows)  # 汇总九种周窗口与阈值情景
    if len(sensitivity_table)!=9 or not sensitivity_table['pair_count'].eq(6).all():  # 核验九情景均覆盖四资产全部6对
        raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 敏感性覆盖不足时停止
    student_portfolio_variance,student_weekly_state_table,student_sensitivity_table = portfolio_variance,weekly_state_table.copy(),sensitivity_table.copy()  # 同时锁定本章计算方差、全部6对状态与九行情景以免后续示例数据覆盖
    strongest_pair = correlation_matrix.where(~np.eye(len(correlation_matrix),dtype=bool)).stack().idxmax()  # 定位最高样本相关资产对
    print({'status':'READY','name_audit':name_audit['status'],'assets':len(observed_codes),'rows':len(return_panel),'period':[str(return_panel.index.min().date()),str(return_panel.index.max().date())],'missing':missing_observations.to_dict()})  # 输出绑定期间与覆盖依据
    print({'strongest_pair':' / '.join(strongest_pair),'correlation':round(float(correlation_matrix.loc[strongest_pair]),3),'portfolio_variance':round(portfolio_variance,6),'risk_budget':risk_budget,'condition_met':portfolio_condition_met})  # 输出关系与满足条件依据
    print({'weekly_pairs':len(weekly_state_table),'sensitivity_scenarios':len(sensitivity_table),'owner':decision_table.at[0,'owner'],'action':decision_table.at[0,'action']})  # 输出比较范围和建议
{'status': 'READY', 'name_audit': 'PASS', 'assets': 4, 'rows': 1698, 'period': ['2019-01-03', '2025-12-31'], 'missing': {'002415.SZ': 0, '600276.SH': 0, '600104.SH': 0, '002230.SZ': 0}}
{'strongest_pair': '002415.SZ / 002230.SZ', 'correlation': 0.481, 'portfolio_variance': 0.039274, 'risk_budget': 0.04, 'condition_met': False}
{'weekly_pairs': 6, 'sensitivity_scenarios': 9, 'owner': '风控负责人', 'action': '保持权重并按周观察'}

计算说明:当前相关关系采用 60 个交易日的滚动相关,并在每周五记录一次。为了观察结论是否受设置影响,再用周收益比较 20、60、120 周窗口和 0.7、0.8、0.9 三组判断标准。连续四周高于设定值,才说明这种高相关关系具有持续性。这些数值只用于课堂练习。

怎样解读风险结果

结果 需要观察什么 怎样解释
协方差矩阵 组合方差是否高于课堂预算 如果明显偏高,应检查权重是否过度集中
60 个交易日滚动相关 是否有股票配对连续四周高于 0.8 连续高相关说明分散风险的作用可能减弱
不同窗口的比较表 结论是否随时间窗口或判断标准大幅改变 如果变化很大,说明结论对参数选择较敏感

数学基础:协方差的定义

总体协方差

\[ \large \text{Cov}(X, Y) = E[(X - \mu_X)(Y - \mu_Y)] \]

样本协方差

\[ \large \text{Cov}(X, Y) = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y}) \]

协方差的基本性质

  • \(\text{Cov}(X, X) = \text{Var}(X)\)(自身的协方差等于方差)
  • \(\text{Cov}(X, Y) = \text{Cov}(Y, X)\)(对称性)
  • \(\text{Cov}(aX, bY) = ab \cdot \text{Cov}(X, Y)\)(双线性)

直觉理解:协方差衡量两个变量的「同步偏离」程度

  • 正值 → 同向变动
  • 负值 → 反向变动
  • 零 → 无线性关联

数学基础:Pearson 相关系数

\[ \large \rho_{XY} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} = \frac{\sum(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum(x_i - \bar{x})^2}\sqrt{\sum(y_i - \bar{y})^2}} \]

核心性质

  • \(-1 \leq \rho \leq 1\)
  • \(\rho = 1\):完全正相关
  • \(\rho = -1\):完全负相关
  • \(\rho = 0\):无线性相关(可能有非线性关系)

协方差 vs 相关系数:对比

特性 协方差 相关系数
量纲 有单位(如%²) 无量纲
范围 \((-\infty, +\infty)\) \([-1, 1]\)
标准化
可比性 不同资产对不可比 所有资产对可比
应用 计算组合方差 衡量关系强度

运行前预测|平台任务1:读取销售数据

  • 业务/数据输入sale_points.csv 的文件路径、字段、编码与行数;pd 是模块别名,read_csv 是函数,都不是业务输入。
  • 结果预测:平台会按固定文本判题;本页受保护代码含非法变量名和减号,不能作为本地 Python 可执行性的依据。
  • 完成要求:分开检查“平台固定录入”与“下页合法 Python 读取”;后者须输出字段、形状和前五行。

⭐ 平台任务1:读取销售数据

# 注:sale_points.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
sale_points-pd.read_csv('sale_points.csv')  # 从CSV文件读取数据
sale points.head()  # 查看sale points前5行数据

任务复盘|平台任务1:读取销售数据

运行后核对:先确认平台是否接受固定录入;不得将其等同于本地运行通过。本地分析应运行下页纠正块,检查赋值符、合法变量名、文件路径、字段与形状。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

正确读取写法:在课件外围完成纠错

上页受教学平台固定答案约束,原代码不能改动。独立分析时应使用合法变量名和赋值运算符:


import pandas as pd  # 导入Pandas以读取结构化销售数据
sale_points = pd.read_csv('sale_points.csv')  # 将CSV内容赋给合法变量名
print(sale_points.head())  # 检查字段与前五行是否符合分析口径

自检:变量名不能含空格;赋值使用 =,不能误写为减号 -

运行前预测|平台任务2:相关分析与协方差分析

  • 业务/数据输入data 中用于计算的两个数值字段、样本期、缺失与异常值;corr_matrix 是中间结果,im 是图形对象,不是业务输入。
  • 结果预测:先写出相关系数的方向/范围、矩阵形状和热力图对角线特征;不得只预测“显著”文字。
  • 完成要求:平台固定录入与本地可执行性分开检查。受保护块中 corr =myfont = 等未完成表达式不能作为本地运行依据。

⭐ 平台任务2:相关分析与协方差分析

展开完整代码(投影默认折叠)
# 注:相关分析sales.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
import pandas as pd
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import numpy as np  # 导入NumPy数值计算库
# 读取数据
data = pd.read_csv('相关分析sales.csv')
# 计算皮尔逊相关系数
corr =
# 计算相关性矩阵
corr_matrix = data.corr()
# 绘制热力图
fig, ax = plt.subplots()
im = ax.imshow(corr_matrix, cmap='YlGnBu')  # 显示热力图矩阵
# 显示相关系数的值
for i in range(corr_matrix.shape[0]):
    for j in range(corr_matrix.shape[1]):  # 遍历range(corr_matrix.shape[1])中的每个j
                text = ax.text(j, i, round(corr_matrix.iloc[i, j],2),  # 按位置索引提取显示相关系数的值
                       ha="center", va="center", color="black")  # 设置标注文本的水平和垂直对齐方式
# 设置坐标轴标签和标题
ax.set_xticks(np.arange(len(corr_matrix.columns)))
ax.set_yticks(np.arange(len(corr_matrix.columns)))  # 设置Y轴刻度标签
ax.set_xticklabels(corr_matrix.columns)  # 设置X轴刻度标签
ax.set_yticklabels(corr_matrix.columns)  # 设置Y轴刻度标签
ax.set_title("Correlation Heatmap")  # 设置图表标题
# 在图形旁添加颜色条
cbar = ax.figure.colorbar(im, ax=ax)
# 显示图形
plt.savefig("热力图.png")
# 输出结果
print('Pearson correlation coefficient: ', corr)
if abs(corr) >= 0.7:  # 条件判断:abs(corr) >= 0.7
    print('存在显著的线性相关性')  # 输出存在显著的线性相关性
else:  # 不满足以上条件时
    print('不存在显著的线性相关性')  # 输出不存在显著的线性相关性
    

#任务二
import numpy as np
from scipy import stats  # 导入SciPy科学计算库
import pandas as pd  # 导入Pandas数据分析库
import statsmodels.api as sm  # 导入统计建模库
import statsmodels.formula.api as smf  # 导入统计建模库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
from statsmodels.stats.multicomp import pairwise_tukeyhsd  # 导入统计建模库
from statsmodels.graphics.api import interaction_plot  # 导入统计建模库
from matplotlib.font_manager import FontProperties  # 导入Matplotlib库
myfont=  # 设置中文字体属性
 
# 从CSV文件读取数据存入sale_points
sale_points = pd.read_csv(u'https://huoran.oss-cn-shenzhen.aliyuncs.com/20221116/csv/1592817699758039040.csv',encoding = "gbk")
 
sale_points['market'] = sale_points['market'].astype('category')  # 转换数据类型
# 定义列表sale_points['market'].cat.categories
sale_points['market'].cat.categories=['market 1', 'market 2', 'market 3']
 
sale_points['warranty'] = sale_points['warranty'].astype('category')  # 转换数据类型
# 定义列表sale_points['warranty'].cat.categories
sale_points['warranty'].cat.categories=['1 years', '3 years']
 
print(sale_points.head())  # 输出前几行数据
 
formula = 'sales ~ points + C(market) * C(warranty)'  # 定义模型公式:sales ~ points + C(market) * C(warranty)
sale_points_anova_cov_est = smf.ols(formula, data = sale_points).fit() # dc_sales_est 是一个模型对象
print(sale_points_anova_cov_est.summary())  # 输出合计值

任务复盘|平台任务2:相关分析与协方差分析

运行后核对:核对数值字段、样本行数、缺失处理、矩阵对称性与对角线为 1。若平台接受固定录入,仍须另用合法表达式复算后才能声称本地可执行。

固定措辞校正:受保护代码会打印“显著”字样,但没有计算 p 值、置信区间或执行假设检验;课堂解释只能写“样本相关系数超过固定描述阈值/未超过”,不得声称统计显著。

拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。

教学示例:协方差矩阵计算

Listing 9
展开中性收益率教学示例
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']
plt.rcParams['axes.unicode_minus'] = False

# 创建中性多资产收益率纯教学示例,不承载企业或投资依据
np.random.seed(42)
n_days = 500

# 各资产的期望日收益率
mean_returns = [0.001, 0.0008, 0.0012, 0.0005]

# 设定教学示例协方差矩阵,非市场观测
cov_matrix_true = np.array([
    [0.0004, 0.0002, 0.00015, 0.0001],
    [0.0002, 0.0003, 0.0001, 0.00005],
    [0.00015, 0.0001, 0.00035, 0.00008],
    [0.0001, 0.00005, 0.00008, 0.0002]
])

# 生成多元正态分布数据
returns = np.random.multivariate_normal(mean_returns, cov_matrix_true, n_days)

# 转换为DataFrame
assets = ['资产A', '资产B', '资产C', '资产D']  # 使用中性资产标签避免企业依据误读
df_returns = pd.DataFrame(returns, columns=assets)

print('教学示例,非市场观测|收益率数据(前10行):')  # 在输出表前声明教学示例边界
print(df_returns.head(10))
教学示例,非市场观测|收益率数据(前10行):
        资产A       资产B       资产C       资产D
0 -0.021316  0.007675 -0.001451 -0.000617
1 -0.006528  0.016661  0.013422 -0.009431
2  0.016949  0.004799 -0.001382  0.004445
3 -0.004128 -0.029601  0.015739  0.019084
4  0.033404  0.001141  0.007167  0.008088
5 -0.016207 -0.029359 -0.014025 -0.017959
6  0.012169  0.004621  0.001933  0.018252
7 -0.001172  0.015955  0.009460  0.022482
8  0.001602 -0.010140  0.018858 -0.012143
9 -0.010412 -0.021935  0.018074  0.019635

计算协方差矩阵与相关系数矩阵

Listing 10
展开协方差与相关矩阵代码
# 计算协方差矩阵
cov_matrix = df_returns.cov()
print('教学示例,非市场观测|协方差矩阵:')  # 在人工协方差输出前声明依据边界
print(cov_matrix.round(6))

# 计算Pearson相关系数矩阵
corr_matrix = df_returns.corr()
print('\n教学示例,非市场观测|相关系数矩阵:')  # 在人工相关矩阵输出前声明依据边界
print(corr_matrix.round(4))
教学示例,非市场观测|协方差矩阵:
          资产A       资产B       资产C       资产D
资产A  0.000361  0.000171  0.000115  0.000072
资产B  0.000171  0.000289  0.000077  0.000033
资产C  0.000115  0.000077  0.000329  0.000055
资产D  0.000072  0.000033  0.000055  0.000179

教学示例,非市场观测|相关系数矩阵:
        资产A     资产B     资产C     资产D
资产A  1.0000  0.5276  0.3322  0.2823
资产B  0.5276  1.0000  0.2494  0.1438
资产C  0.3322  0.2494  1.0000  0.2283
资产D  0.2823  0.1438  0.2283  1.0000

相关性可视化:热力图

展开相关性热力图代码
plt.figure(figsize=(8.6, 4.2))
sns.heatmap(corr_matrix,
            annot=True, fmt='.2f',
            cmap='RdYlGn', center=0,
            square=True, linewidths=0.5,
            cbar_kws={'label': '教学示例,非市场观测|相关系数'},  # 在色标持续声明依据边界
            vmin=-1, vmax=1)
plt.title('教学示例,非市场观测|资产收益率相关系数矩阵', fontsize=16, fontweight='bold', pad=20)  # 在导出图标题保留依据边界
plt.tight_layout()
plt.show()
教学示例,非市场观测。人工预设的四资产相关矩阵以红蓝热力图显示。
Figure 1: 教学示例,非市场观测|相关矩阵热力图

找出相关性最高/最低的资产组合

Listing 11
展开最高与最低相关资产对代码
# 提取上三角矩阵(排除对角线),避免重复
mask = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)
corr_upper = corr_matrix.where(mask)

# 找最大和最小相关系数
max_corr = corr_upper.max().max()
min_corr = corr_upper.min().min()
max_pair = corr_upper.stack().idxmax()
min_pair = corr_upper.stack().idxmin()

print(f'教学示例,非市场观测|最高相关性: {max_pair} = {max_corr:.4f}')  # 在孤立输出保留依据边界
print(f'最低相关性: {min_pair} = {min_corr:.4f}')
教学示例,非市场观测|最高相关性: ('资产A', '资产B') = 0.5276
最低相关性: ('资产B', '资产D') = 0.1438

热力图配色方案选择

方案 特点 适用场景
RdYlGn 红-黄-绿,中心为0 一般相关性
coolwarm 蓝-白-红,中心为0 科学出版
viridis 紫-黄,感知均匀 色盲友好
RdBu 红-蓝,发散 正负对比明显

组合风险与协方差:投资组合优化

组合方差公式(矩阵形式):

\[ \large \sigma_p^2 = \mathbf{w}^T \Sigma \mathbf{w} \]

其中:

  • \(\mathbf{w}\):权重向量 \((n \times 1)\)
  • \(\Sigma\):协方差矩阵 \((n \times n)\)

分散化原理

对非负权重、有限波动率且所有相关系数不超过 1 的组合:

\[ \large \sigma_p^2=\sum_i w_i^2\sigma_i^2+2\sum_{i<j}w_iw_j\rho_{ij}\sigma_i\sigma_j\leq\left(\sum_i w_i\sigma_i\right)^2 \]

当至少一对正权重、正波动资产满足 \(\rho_{ij}<1\) 时,右侧不等式严格;这表示组合波动率低于各资产波动率的加权和。它不保证组合方差小于 \(\sum_iw_i^2\sigma_i^2\),因为正协方差交叉项仍会增加方差。

数值反例:两资产均为 \(w=0.5,\sigma=1,\rho=0.5\) 时,\(\sigma_p^2=0.75\),大于 \(\sum_iw_i^2\sigma_i^2=0.5\),但仍小于 \(\left(\sum_iw_i\sigma_i\right)^2=1\)

教学示例:生成随机权重点云

展开随机组合生成代码
assets_list = ['资产A', '资产B', '资产C', '资产D']  # 固定中性资产顺序
n_assets = len(assets_list)

# 日协方差 → 年化协方差(×252个交易日)
cov_annual = cov_matrix * 252

# 生成1000个随机组合
np.random.seed(42)
n_portfolios = 1000

weights_list = []
returns_list = []
risks_list = []

for _ in range(n_portfolios):
    weights = np.random.random(n_assets)
    weights = weights / np.sum(weights)  # 归一化,权重和为1

    # 组合期望收益(年化)
    portfolio_return = np.sum(df_returns.mean() * weights) * 252

    # 组合方差 = w^T * Σ * w
    portfolio_variance = np.dot(weights.T, np.dot(cov_annual, weights))
    portfolio_std = np.sqrt(portfolio_variance)

    weights_list.append(weights)
    returns_list.append(portfolio_return)
    risks_list.append(portfolio_std)

df_portfolios = pd.DataFrame({
    '收益率': returns_list,
    '风险(标准差)': risks_list
})
Figure 2

可视化随机权重点云(非有效前沿估计)

展开有效前沿绘图代码
plt.figure(figsize=(9, 4.2))
scatter = plt.scatter(
    df_portfolios['风险(标准差)'], df_portfolios['收益率'],
    c=df_portfolios['收益率'] / df_portfolios['风险(标准差)'],
    cmap='viridis', alpha=0.6, s=50
)
plt.colorbar(scatter, label='教学示例,非市场观测|收益风险比')  # 在颜色图例声明依据边界

# 标记单个资产
for i, asset in enumerate(assets_list):
    asset_return = df_returns[asset].mean() * 252
    asset_risk = np.sqrt(cov_annual.iloc[i, i])
    plt.scatter(asset_risk, asset_return, s=200, marker='*',
                color='red', edgecolors='black', linewidths=1.5)
    plt.text(asset_risk, asset_return, f'  {asset}', fontsize=16)

plt.xlabel('年化风险(标准差)', fontsize=16)
plt.ylabel('年化收益率', fontsize=16)
plt.title('教学示例,非市场观测|投资组合风险与收益点云', fontsize=16, fontweight='bold')  # 在导出图标题保留依据边界
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
教学示例,非市场观测。人工收益与协方差下随机权重组合的风险收益点云。
Figure 3: 教学示例,非市场观测|随机权重风险收益点云

教学示例:样本内最高收益风险比

Listing 12
展开最大夏普比率组合代码
max_sharpe_idx = (df_portfolios['收益率'] / df_portfolios['风险(标准差)']).idxmax()
optimal_weights = weights_list[max_sharpe_idx]

print('教学示例,非市场观测|样本内最高收益风险比权重:')  # 输出示例数据内描述结果而非投资建议
for asset, weight in zip(assets_list, optimal_weights):
    print(f'{asset}: {weight:.2%}')
教学示例,非市场观测|样本内最高收益风险比权重:
资产A: 1.73%
资产B: 71.54%
资产C: 8.59%
资产D: 18.14%

时间序列相关性:滚动相关

展开60日滚动相关代码
# 计算中性资产A与资产B的60日滚动相关系数
window = 60
rolling_corr = df_returns['资产A'].rolling(window).corr(df_returns['资产B'])  # 计算中性示例数据滚动相关

plt.figure(figsize=(10, 4.0))
plt.plot(df_returns.index, rolling_corr, linewidth=2, color='#2E86AB')

# 参考线
plt.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
plt.axhline(y=rolling_corr.mean(), color='red', linestyle='--',
            linewidth=2, label=f'教学示例,非市场观测|均值={rolling_corr.mean():.4f}')  # 在图例声明依据边界

# 填充正负相关区域
plt.fill_between(df_returns.index, rolling_corr, 0,
                 where=(rolling_corr >= 0), alpha=0.3, color='green', label='教学示例,非市场观测|正相关期')  # 在图例保留依据边界
plt.fill_between(df_returns.index, rolling_corr, 0,
                 where=(rolling_corr < 0), alpha=0.3, color='red', label='教学示例,非市场观测|负相关期')  # 在图例保留依据边界

plt.title('教学示例,非市场观测|滚动相关系数(60日窗口)', fontsize=16, fontweight='bold')  # 在导出图标题保留依据边界
plt.xlabel('日期', fontsize=16)
plt.ylabel('相关系数', fontsize=16)
plt.legend(fontsize=16, loc='upper left', title='教学示例,非市场观测')  # 在滚动相关图例标题持续声明依据边界
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
教学示例,非市场观测。两条人工收益序列的滚动相关曲线与正负区域。
Figure 4: 教学示例,非市场观测|滚动相关系数

滚动相关:统计分析

Listing 13
展开滚动相关统计代码
print('教学示例,非市场观测|相关系数统计:')  # 在孤立输出表头保留依据边界
print(f'均值: {rolling_corr.mean():.4f}')
print(f'标准差: {rolling_corr.std():.4f}')
print(f'最大值: {rolling_corr.max():.4f}')
print(f'最小值: {rolling_corr.min():.4f}')

# 识别相关系数跌破课堂阈值的时期;未做统计检验,不称显著
threshold = rolling_corr.mean() - 2 * rolling_corr.std()
low_corr_periods = rolling_corr[rolling_corr < threshold]
print(f'\n相关性异常低的时期({len(low_corr_periods)}天):')
print(low_corr_periods.head())
教学示例,非市场观测|相关系数统计:
均值: 0.5228
标准差: 0.1118
最大值: 0.6752
最小值: 0.2269

相关性异常低的时期(28天):
397    0.289964
398    0.298138
399    0.298487
401    0.279462
402    0.275398
dtype: float64

滚动窗口的选择

窗口 天数 特点 适用场景
短期 20日 捕捉快速变化,噪声大 短线交易
中期 60日 平衡灵敏度和稳定性 一般分析
长期 120日 平滑,识别长期关系 战略配置

教学示例:类别相关结构

下列矩阵为人为指定的中性数值,只演示掩码、热力图和成对检索;不对应真实行业,不发布轮动或配置结论。

展开行业相关矩阵示例代码
industries_network = ['类别A', '类别B', '类别C', '类别D', '类别E', '类别F']  # 使用中性类别标签
n_industries = len(industries_network)

# 构建行业相关系数矩阵
industry_corr = pd.DataFrame(
    [[1.00, 0.30, 0.35, 0.25, 0.40, 0.60],
     [0.30, 1.00, 0.55, 0.30, 0.25, 0.20],
     [0.35, 0.55, 1.00, 0.35, 0.20, 0.25],
     [0.25, 0.30, 0.35, 1.00, 0.15, 0.20],
     [0.40, 0.25, 0.20, 0.15, 1.00, 0.35],
     [0.60, 0.20, 0.25, 0.20, 0.35, 1.00]],
    index=industries_network, columns=industries_network
)

print('教学示例,非市场观测|中性类别相关矩阵:')  # 输出人为矩阵供结构检查
print(industry_corr)
教学示例,非市场观测|中性类别相关矩阵:
      类别A   类别B   类别C   类别D   类别E   类别F
类别A  1.00  0.30  0.35  0.25  0.40  0.60
类别B  0.30  1.00  0.55  0.30  0.25  0.20
类别C  0.35  0.55  1.00  0.35  0.20  0.25
类别D  0.25  0.30  0.35  1.00  0.15  0.20
类别E  0.40  0.25  0.20  0.15  1.00  0.35
类别F  0.60  0.20  0.25  0.20  0.35  1.00
Figure 5

中性类别相关热力图

展开行业热力图代码
plt.figure(figsize=(8.6, 4.2))
mask = np.triu(np.ones_like(industry_corr, dtype=bool))
sns.heatmap(industry_corr,
            annot=True, fmt='.2f',
            cmap='RdYlGn', center=0,
            square=True, linewidths=0.5,
            mask=mask,
            cbar_kws={'label': '教学示例,非市场观测|相关系数'},  # 在中性类别色标持续声明依据边界
            vmin=-1, vmax=1)
plt.xticks(rotation=28, ha='right')  # 旋转行业标签并右对齐,避免横轴文字连写
plt.yticks(rotation=0)  # 保持纵轴行业标签水平可读
plt.title('教学示例,非市场观测|中性类别相关结构', fontsize=16, fontweight='bold')  # 标明图形依据身份
plt.tight_layout()
plt.show()
教学示例,非市场观测。六个中性类别的人工相关矩阵下三角演示掩码和标注。
Figure 6: 教学示例,非市场观测|中性类别相关下三角

识别超过阈值的中性类别对

Listing 14
展开高相关行业对识别代码
high_corr_pairs = []
for i in range(n_industries):
    for j in range(i+1, n_industries):
        corr_val = industry_corr.iloc[i, j]
        if abs(corr_val) > 0.5:
            high_corr_pairs.append((industries_network[i], industries_network[j], corr_val))

print('教学示例,非市场观测|超过课堂阈值的中性类别对(|ρ| > 0.5):')  # 输出带依据边界的阈值检索结果
for pair in sorted(high_corr_pairs, key=lambda x: abs(x[2]), reverse=True):
    print(f'{pair[0]} - {pair[1]}: {pair[2]:.2f}')
教学示例,非市场观测|超过课堂阈值的中性类别对(|ρ| > 0.5):
类别A - 类别F: 0.60
类别B - 类别C: 0.55

教学示例的解释边界

本页只能检查代码是否正确提取矩阵上三角与阈值对:

  • 可以回答:哪些人为矩阵单元超过 0.5,排序是否与手工读取一致。
  • 不能回答:真实行业是否同涨同跌、能否分散风险或预判轮动。
  • 拓展应用前提:替换为有来源、期间和同一时间段样本数的真实收益后,重新估计并报告不确定性。

偏相关系数:控制第三方变量

偏相关系数 \(\rho_{XY|Z}\):在给定线性模型下,剔除变量 \(Z\) 线性部分后,\(X\)\(Y\) 残差的样本相关性;它不等于真实因果关系。

金融应用

  • Alpha分离:剔除市场因子后的特质收益
  • 因子正交化:构建正交因子
  • 归因分析:分离不同来源的影响

纯教学示例:偏相关系数计算

以下随机数仅用于验证“回归取残差再求相关”的计算步骤,不对应真实证券、行业或市场,也不得据此发布企业、行业或投资结论。

Listing 15
展开偏相关演示数据代码
from scipy.stats import pearsonr
from sklearn.linear_model import LinearRegression

np.random.seed(42)
n = 500

# 构造三变量数据
market = np.random.normal(0.001, 0.02, n)
industry_factor = np.random.normal(0, 0.01, n)
stock = 0.8 * market + 0.5 * industry_factor + np.random.normal(0, 0.015, n)

df_partial = pd.DataFrame({
    '市场': market,
    '个股': stock,
    '行业因子': industry_factor
})

# 简单相关系数
corr_market_stock = df_partial['市场'].corr(df_partial['个股'])
corr_industry_stock = df_partial['行业因子'].corr(df_partial['个股'])

print('教学示例,非市场观测|简单相关系数:')  # 在孤立输出表头保留依据边界
print(f'市场-个股: {corr_market_stock:.4f}')
print(f'行业因子-个股: {corr_industry_stock:.4f}')
教学示例,非市场观测|简单相关系数:
市场-个股: 0.6652
行业因子-个股: 0.2234

偏相关系数:回归残差法

Listing 16
展开回归残差法代码
# 个股对行业因子回归,取残差
model_stock = LinearRegression().fit(
    df_partial[['行业因子']], df_partial['个股']
)
residual_stock = df_partial['个股'] - model_stock.predict(df_partial[['行业因子']])

# 市场对行业因子回归,取残差
model_market = LinearRegression().fit(
    df_partial[['行业因子']], df_partial['市场']
)
residual_market = df_partial['市场'] - model_market.predict(df_partial[['行业因子']])

# 偏相关系数 = 残差的相关系数
partial_corr = np.corrcoef(residual_market, residual_stock)[0, 1]

print(f'教学示例,非市场观测|偏相关系数(控制行业因子):')  # 在孤立输出表头保留依据边界
print(f'市场-个股: {partial_corr:.4f}')
print(f'解释: 剔除示例数据中第三变量的线性部分后,计算两组残差的样本相关性')  # 输出教学示例的统计边界而非业务结论
教学示例,非市场观测|偏相关系数(控制行业因子):
市场-个股: 0.7018
解释: 剔除示例数据中第三变量的线性部分后,计算两组残差的样本相关性

本章小结

  • 协方差:衡量两变量同步偏离程度,有量纲
  • 相关系数:标准化后的协方差,范围 \([-1, 1]\)
  • 热力图:快速识别多资产相关性模式
  • 组合风险\(\sigma_p^2 = \mathbf{w}^T \Sigma \mathbf{w}\),协方差决定分散化效果
  • 滚动相关:捕捉相关性的时变特征
  • 偏相关:控制第三方变量线性部分后的残差样本关系,不自动代表因果

随堂练习

  • 问题 1|当前相关关系如何?:用日收益计算 60 个交易日滚动相关,并在每周五记录一次。哪些股票配对的相关系数高于 0.8?这种情况持续了几周?
  • 问题 2|风险结果是否稳定?:写出资产完整性、权重和、\(\mathbf{w}^T\Sigma\mathbf{w}\) 双重求和复算,以及周收益20/60/120周×0.7/0.8/0.9的九行情景表。
  • 问题 3|换一组股票,结论是否相同?:读取五粮液、恒瑞医药、长江电力和美的集团 2019—2025 年的价格,计算全部六组配对的相关系数和等权组合方差,并与主案例比较。
  • 作答提示:60 个交易日滚动相关用于描述近期关系;20、60、120 周的比较用于观察结果对时间窗口是否敏感。0.8 是课堂练习中的判断标准,不是显著性检验。

教师参考解答|组合风险矩阵

教师参考解答|答案与说明 1

  • 所用数据与字段:主案例和拓展案例均使用 2019—2025 年前复权日价格,四只股票等权。组合方差由最近 60 个交易日的日收益协方差计算,并乘以 252 转为年化结果;不同窗口的比较覆盖全部六组股票配对。
  • 推导:年化协方差矩阵 \(\Sigma_{252}=252\operatorname{Cov}(R_{t,day})\);年化组合方差 \(\sigma_p^2=\mathbf w^\top\Sigma_{252}\mathbf w=\sum_i\sum_jw_iw_j\Sigma_{ij}\),预算0.04与方差同为“年化日收益方差”单位。

教师参考解答|代码 1

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具以定位学生本章计算同一前复权快照
import numpy as np  # 导入矩阵工具以计算与核对组合方差
import pandas as pd  # 导入表格工具以构造日/周同一时间段的收益
price_path=Path('/home/ubuntu/r2_data_mount/data/stock/stock_price_pre_adjusted.h5')  # 绑定学生本章计算同一前复权HDF快照
main_codes=['002415.SZ','600276.SH','600104.SH','002230.SZ']  # 固定学生本章计算四资产与顺序
alternative_codes=['000858.SZ','600276.SH','600900.SH','000333.SZ']  # 固定第二组四只中国资产与顺序
period_start=pd.Timestamp('2019-01-01')  # 固定共同样本起点
period_end=pd.Timestamp('2025-12-31')  # 固定共同样本终点
annualization_factor=252  # 固定日收益协方差年化因子
risk_budget=.04  # 固定年化20%波动对应的方差预算
positive_threshold=.8  # 固定高正相关课堂阈值
if not price_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 来源缺失时请先检查数据后再继续

教师参考解答|代码 2

展开代码(代码区可独立滚动)
def load_close_panel(asset_codes):  # 定义两组资产共用的读取与质量要求函数
    raw_codes=[code.replace('.SH','.XSHG').replace('.SZ','.XSHE') for code in asset_codes]  # 转换HDF证券后缀
    rows=pd.read_hdf(price_path,key='data',where='order_book_id in raw_codes',columns=['close']).reset_index()  # 从同一前复权快照读取目标收盘价
    required={'order_book_id','date','close'}  # 声明共用最小字段结构
    if not required.issubset(rows.columns): raise ValueError(f'输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 字段不全时请先检查数据后再继续
    rows=rows.rename(columns={'order_book_id':'ts_code','date':'trade_date'})  # 统一业务字段名
    rows['ts_code']=rows['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 恢复课程代码后缀
    rows['trade_date']=pd.to_datetime(rows['trade_date'],errors='coerce')  # 统一交易日期类型
    rows['close']=pd.to_numeric(rows['close'],errors='coerce')  # 统一收盘价数值类型
    rows=rows.loc[rows['ts_code'].isin(asset_codes)&rows['trade_date'].between(period_start,period_end)]  # 锁定资产与2019—2025期间
    if set(rows['ts_code'].dropna().unique())!=set(asset_codes): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 资产覆盖失败时停止
    if rows.duplicated(['trade_date','ts_code']).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 唯一键失败时停止
    if rows['trade_date'].isna().any() or not np.isfinite(rows['close']).all() or (rows['close']<=0).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 日期或数值要求失败时停止
    panel=rows.pivot(index='trade_date',columns='ts_code',values='close').reindex(columns=asset_codes).dropna().sort_index()  # 构造固定顺序共同交易日面板
    if len(panel)<120 or panel.index.min()>pd.Timestamp('2019-01-31') or panel.index.max()<pd.Timestamp('2025-01-01'): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 期间与窗口不足时停止
    return panel  # 返回通过五项要求的同一时间段的价格

教师参考解答|代码 3

展开代码(代码区可独立滚动)
def all_pair_states(daily_returns):  # 定义全部资产对60交易日周五状态函数
    state_rows=[]  # 初始化全部资产对当前状态
    for left_position,left_code in enumerate(daily_returns.columns):  # 逐个选择左资产
        for right_code in daily_returns.columns[left_position+1:]:  # 仅选择不重复右资产
            rolling=daily_returns[left_code].rolling(60).corr(daily_returns[right_code]).dropna().resample('W-FRI').last().dropna()  # 计算60交易日滚动相关并抽取周五状态
            breach=rolling.gt(positive_threshold)  # 只标记高正相关而不取绝对值
            streak=breach.groupby((~breach).cumsum()).cumsum().astype(int)  # 计算高正相关连续周数
            state_rows.append({'asset_pair':f'{left_code} / {right_code}','week_end':rolling.index[-1],'correlation_60d':rolling.iloc[-1],'positive_threshold':positive_threshold,'continuous_weeks':int(streak.iloc[-1]),'four_week_condition_met':bool(streak.iloc[-1]>=4)})  # 保存资产对当前状态
    return pd.DataFrame(state_rows)  # 返回全部唯一资产对状态表

def all_pair_sensitivity(weekly_returns):  # 定义全部资产对周窗口敏感性函数
    rows=[]  # 初始化九情景结果
    for window_weeks in [20,60,120]:  # 遍历三种周收益窗口
        for threshold in [.7,.8,.9]:  # 遍历三种高正相关阈值
            pair_conditions=[]  # 初始化每个唯一资产对的满足条件序列
            for left_position,left_code in enumerate(weekly_returns.columns):  # 逐个选择左资产
                for right_code in weekly_returns.columns[left_position+1:]:  # 仅选择不重复右资产
                    rolling=weekly_returns[left_code].rolling(window_weeks).corr(weekly_returns[right_code])  # 计算指定周窗口相关
                    pair_conditions.append(rolling.gt(threshold).rolling(4,min_periods=4).sum().eq(4))  # 记录连续四周高正相关满足条件
            rows.append({'window_weeks':window_weeks,'positive_threshold':threshold,'pair_count':len(pair_conditions),'qualifying_pairs':sum(int(series.any()) for series in pair_conditions),'qualifying_weeks':sum(int(series.sum()) for series in pair_conditions)})  # 汇总全部资产对情景
    return pd.DataFrame(rows)  # 返回九行情景表

教师参考解答|代码 4

展开代码(代码区可独立滚动)
main_close=load_close_panel(main_codes)  # 用共用函数重新计算学生本章计算同一时间段的价格
main_daily=main_close.pct_change(fill_method=None).dropna()  # 重新计算学生本章计算日收益
main_weekly=main_close.resample('W-FRI').last().pct_change(fill_method=None).dropna()  # 重新计算学生本章计算周收益
if len(main_daily)<80 or len(main_weekly)<120: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行60交易日与120周样本要求
if not np.isfinite(main_daily).all().all() or not np.isfinite(main_weekly).all().all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行收益数值要求
main_weights=pd.Series(.25,index=main_codes,name='weight')  # 重新计算学生本章计算四资产等权
if (main_weights<0).any() or not np.isclose(main_weights.sum(),1): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行非负与和为1权重要求
main_covariance=main_daily.tail(60).cov()*annualization_factor  # 重新计算最近60交易日乘252的年化协方差
main_variance=float(main_weights@main_covariance@main_weights)  # 重新计算矩阵二次型年化方差
main_manual=sum(main_weights.iloc[i]*main_weights.iloc[j]*main_covariance.iloc[i,j] for i in range(4) for j in range(4))  # 用16项双重求和独立复算
main_states=all_pair_states(main_daily)  # 重新计算全部6对60交易日周五状态
main_sensitivity=all_pair_sensitivity(main_weekly)  # 重新计算全部6对九行情景
main_high_corr_pairs=main_states.loc[main_states['four_week_condition_met'],'asset_pair'].tolist()  # 提取本章计算当前高正相关满足条件对
assert list(main_close.columns)==main_codes and np.isclose(main_weights.sum(),1)  # 检查资产顺序与权重和同学生本章计算
assert np.allclose(main_covariance,main_daily.tail(60).cov()*252) and np.isclose(main_variance,main_manual)  # 检查60交易日、252年化与方差复算
assert len(main_states)==6 and main_states['asset_pair'].nunique()==6 and main_states['positive_threshold'].eq(.8).all()  # 检查基准状态覆盖全部6对且统一高正相关阈值
assert len(main_sensitivity)==9 and main_sensitivity['pair_count'].eq(6).all() and set(main_sensitivity['window_weeks'])=={20,60,120}  # 检查九情景与全部6对覆盖
if 'student_portfolio_variance' in globals(): assert np.isclose(main_variance,student_portfolio_variance)  # 本章计算已执行时检查教师方差逐值一致
if 'student_weekly_state_table' in globals(): assert main_states.set_index('asset_pair')['four_week_condition_met'].equals(student_weekly_state_table.set_index('asset_pair')['four_week_condition_met'])  # 本章计算已执行时检查全部6对满足条件一致
if 'student_sensitivity_table' in globals(): assert main_sensitivity.equals(student_sensitivity_table)  # 本章计算已执行时检查九情景全部字段逐值一致
print({'资产':main_codes,'权重和':main_weights.sum(),'方差单位':'年化日收益方差','annualization_factor':252,'portfolio_variance':main_variance,'risk_budget':risk_budget,'high_correlation_pairs':main_high_corr_pairs},main_sensitivity)  # 输出本章计算结果,便于核对

教师参考解答|代码 5

展开代码(代码区可独立滚动)
alternative_close=load_close_panel(alternative_codes)  # 用同一函数读取第二组四只中国资产
alternative_daily=alternative_close.pct_change(fill_method=None).dropna()  # 按同一方法计算新案例日收益
alternative_weekly=alternative_close.resample('W-FRI').last().pct_change(fill_method=None).dropna()  # 按同一方法计算新案例周收益
if len(alternative_daily)<80 or len(alternative_weekly)<120: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行新案例样本要求
if not np.isfinite(alternative_daily).all().all() or not np.isfinite(alternative_weekly).all().all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行新案例数值要求
alternative_weights=pd.Series(.25,index=alternative_codes,name='weight')  # 使用与本章计算相同的四资产等权
if (alternative_weights<0).any() or not np.isclose(alternative_weights.sum(),1): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 执行新案例权重要求
alternative_covariance=alternative_daily.tail(60).cov()*annualization_factor  # 使用同一60交易日乘252方差口径
alternative_variance=float(alternative_weights@alternative_covariance@alternative_weights)  # 计算新案例年化组合方差
alternative_manual=sum(alternative_weights.iloc[i]*alternative_weights.iloc[j]*alternative_covariance.iloc[i,j] for i in range(4) for j in range(4))  # 用16项双重求和复算新案例方差
alternative_states=all_pair_states(alternative_daily)  # 使用同一函数覆盖新案例全部6对
alternative_sensitivity=all_pair_sensitivity(alternative_weekly)  # 使用同一函数覆盖新案例九情景与全部6对
alternative_condition_met_pairs=alternative_states.loc[alternative_states['four_week_condition_met'],'asset_pair'].tolist()  # 提取新案例当前高正相关满足条件对
alternative_corr_condition_met=bool(alternative_states['four_week_condition_met'].any())  # 判断任一资产对是否连续四周高正相关
alternative_variance_condition_met=bool(alternative_variance>risk_budget)  # 判断同单位年化方差是否超过0.04预算
alternative_condition_met=alternative_corr_condition_met or alternative_variance_condition_met  # 汇总相关性和组合方差两项判断
assert list(alternative_close.columns)==alternative_codes and len(alternative_states)==6 and alternative_states['asset_pair'].nunique()==6  # 检查第二组四资产与全部6对
assert np.isclose(alternative_variance,alternative_manual) and len(alternative_sensitivity)==9 and alternative_sensitivity['pair_count'].eq(6).all()  # 检查新案例方差与九情景覆盖
alternative_audit=pd.DataFrame([{'data_fields_ok':'PASS','assets_ok':'PASS','period_ok':'PASS','values_ok':'PASS','weights_ok':'PASS','asset_group':' / '.join(alternative_codes),'asset_pair_count':6,'state_window_days':60,'state_sampling':'W-FRI','sensitivity_windows_weeks':'20/60/120','annualization_factor':252,'variance_unit':'年化日收益方差','portfolio_variance':alternative_variance,'risk_budget':risk_budget,'positive_threshold':.8,'correlation_condition':'correlation_60d > 0.8 连续四周','high_correlation_pairs':alternative_condition_met_pairs,'correlation_condition_met':alternative_corr_condition_met,'variance_condition_met':alternative_variance_condition_met,'condition_met':alternative_condition_met,'owner':'组合风控负责人','action':'核对共同因子并降低集中权重' if alternative_condition_met else '维持等权并按周监控','review_note':'全部6对不再连续四周高于0.8且年化方差回到0.04内;季度再次检查'}])  # 形成同字段新案例处理建议表
assert alternative_audit.at[0,'asset_pair_count']==6 and alternative_audit.at[0,'positive_threshold']==positive_threshold and alternative_audit.at[0,'risk_budget']==risk_budget  # 逐项检查说明信息可核对
print(alternative_audit.T,alternative_sensitivity)  # 输出第二组四只股票及全部六组配对的比较结果

教师参考解答|答案与说明 2

  • 解释答案:每周五记录的 60 个交易日滚动相关用来描述近期关系;20、60、120 周的结果用来观察时间窗口改变后结论是否稳定。只有相关系数连续四周高于设定值,才说明高正相关具有持续性;强负相关不属于这种情况。
  • 拓展应用答案:第二组实际读取五粮液、恒瑞医药、长江电力和美的集团,复用同一读取、全部资产对状态与敏感性函数,同为4资产等权、6对、60交易日乘252和0.04年化方差预算,并输出统一决策信息。
  • 参考结果:主案例和拓展案例均应给出四只股票、六组配对、九种窗口与判断标准组合、权重之和、年化组合方差和双重求和结果。另选一组股票逐周查看相关系数,确认程序是否正确识别“连续四周高于设定值”。
  • 常见错误:混用前复权与后复权价格;股票数量或顺序不一致;用周收益方差乘 52 代替本章规定的 60 个交易日日收益方差;只计算一组股票;把协方差上升误当作相关系数连续高于设定值。